package com.shortLinked.project.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.text.StrBuilder;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.shortLinked.project.common.constant.RedisKeyConstant;
import com.shortLinked.project.common.convention.exception.ClientException;
import com.shortLinked.project.common.convention.exception.ServiceException;
import com.shortLinked.project.common.enums.ValidDateTypeEnum;
import com.shortLinked.project.config.GotoDomainWhiteListConfiguration;
import com.shortLinked.project.dao.entity.ShortLinkDO;
import com.shortLinked.project.dao.entity.ShortLinkGoToDO;
import com.shortLinked.project.dao.mapper.*;
import com.shortLinked.project.dto.biz.ShortLinkStatsRecordDTO;
import com.shortLinked.project.dto.req.ShortLinkBatchCreateReqDTO;
import com.shortLinked.project.dto.req.ShortLinkCreateReqDTO;
import com.shortLinked.project.dto.req.ShortLinkPageReqDTO;
import com.shortLinked.project.dto.req.ShortLinkUpdateReqDTO;
import com.shortLinked.project.dto.resp.*;
import com.shortLinked.project.mq.producer.ShortLinkStatsSaveProducer;
import com.shortLinked.project.service.LinkStatsTodayService;
import com.shortLinked.project.service.ShortLinkService;
import com.shortLinked.project.toolkit.HashUtil;
import com.shortLinked.project.toolkit.ShortLinkUtil;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import static com.shortLinked.project.common.constant.RedisKeyConstant.*;

/**
 * shortlink 接口实现层
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ShortLinkServiceImpl extends ServiceImpl<ShortLinkMapper, ShortLinkDO> implements ShortLinkService {

    private final RBloomFilter<String> shortUriCreateCachePenetrationBloomFilter;

    private final ShortLinkGoToMapper shortLinkGoToMapper;

    private final StringRedisTemplate stringRedisTemplate;

    private final RedissonClient redissonClient;

    private final LinkAccessStatsMapper linkAccessStatsMapper;

    private final LinkLocaleStatsMapper linkLocaleStatsMapper;

    private final LinkOsStatsMapper linkOsStatsMapper;

    private final LinkBrowserStatsMapper linkBrowserStatsMapper;

    private final LinkAccessLogsMapper linkAccessLogsMapper;

    private final LinkDeviceStatsMapper linkDeviceStatsMapper;

    private final LinkNetworkStatsMapper linkNetworkStatsMapper;

    private final LinkStatsTodayMapper linkStatsTodayMapper;

    private final LinkStatsTodayService linkStatsTodayService;

    private final ShortLinkStatsSaveProducer shortLinkStatsSaveProducer;

    private final GotoDomainWhiteListConfiguration gotoDomainWhiteListConfiguration;


    @Value("${short-link.domain.default}")
    private String createShortLinkDefaultDomain;

    @Override
    public ShortLinkBatchCreateRespDTO batchCreateShortLink(ShortLinkBatchCreateReqDTO requestParam) {
        List<String> originUrls = requestParam.getOriginUrls();
        List<String> describes = requestParam.getDescribes();
        // 批量创建短链接返回基本数据
        List<ShortLinkBaseInfoRespDTO> result = new ArrayList<>();
        for (int i = 0; i < originUrls.size(); i++) {
            ShortLinkCreateReqDTO shortLinkCreateReqDTO = BeanUtil.toBean(requestParam, ShortLinkCreateReqDTO.class);
            shortLinkCreateReqDTO.setOriginUrl(originUrls.get(i));
            shortLinkCreateReqDTO.setDescribe(describes.get(i));
            try {
                ShortLinkCreateRespDTO shortLinkCreateRespDTO = createShortLink(shortLinkCreateReqDTO);
                ShortLinkBaseInfoRespDTO linkBaseInfoRespDTO = ShortLinkBaseInfoRespDTO.builder()
                        .describe(describes.get(i))
                        .originUrl(originUrls.get(i))
                        .fullShortUrl(shortLinkCreateRespDTO.getFullShortUrl())
                        .build();
                result.add(linkBaseInfoRespDTO);
            } catch (Throwable ex) {
                log.error("批量创建短链接失败，原始链接参数为：{}", originUrls.get(i));
            }
        }
        return ShortLinkBatchCreateRespDTO.builder()
                .total(result.size())
                .baseLinkInfos(result)
                .build();
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO requestParam) {
        // 验证跳转链接是否添加白名单配置
        verificationWhitelist(requestParam.getOriginUrl());

        String shortLinkSuffix = generateSuffix(requestParam);
        // 整个短链接  ->  域名 + 短链接
        String fullShortUrl = StrBuilder.create(createShortLinkDefaultDomain)
                .append("/")
                .append(shortLinkSuffix)
                .toString();

        ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                .domain(createShortLinkDefaultDomain)
                .originUrl(requestParam.getOriginUrl())
                .gid(requestParam.getGid())
                .createdType(requestParam.getCreatedType())
                .validDateType(requestParam.getValidDateType())
                .validDate(requestParam.getValidDate())
                .describe(requestParam.getDescribe())
                .shortUri(shortLinkSuffix)   // 短链接
                .fullShortUrl(fullShortUrl)
                .enableStatus(0)   // 是否启用
                .favicon(getFavicon(requestParam.getOriginUrl()))  // 获取网站的图标 favicon
                .totalPv(0)
                .totalUv(0)
                .totalUip(0)
                .delTime(0L)
                .build();
        // 短链接跳转对象
        ShortLinkGoToDO shortLinkGoToDO = ShortLinkGoToDO.builder()
                .fullShortUrl(fullShortUrl)
                .gid(requestParam.getGid())
                .build();
        try {
            baseMapper.insert(shortLinkDO);
            // 路由表的插入
            shortLinkGoToMapper.insert(shortLinkGoToDO);
        } catch (DuplicateKeyException ex) {
            // 数据库判断是否存在该短链接  -  不需要进行判读，布隆过滤器生成短链接时，一定是不存在的
            throw new ServiceException(String.format("短链接：%s 重复生成", fullShortUrl));
        }
        // 新增完整的短链接入布隆过滤器 + 缓存（缓存预热）
        stringRedisTemplate.opsForValue().set(
                String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, fullShortUrl),
                requestParam.getOriginUrl(),
                ShortLinkUtil.getLinkValidTime(requestParam.getValidDate()),
                TimeUnit.MILLISECONDS
        );
        shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl);
        return ShortLinkCreateRespDTO.builder()
                .gid(requestParam.getGid())
                .originUrl(requestParam.getOriginUrl())
                .fullShortUrl("http://" + shortLinkDO.getFullShortUrl())
                .build();
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public ShortLinkCreateRespDTO createShortLinkByLock(ShortLinkCreateReqDTO requestParam) {
        // 验证跳转链接是否添加白名单配置
        verificationWhitelist(requestParam.getOriginUrl());
        String fullShortUrl;
        RLock lock = redissonClient.getLock(SHORT_LINK_CREATE_LOCK_KEY);
        lock.lock();
        try {
            String shortLinkSuffix = generateSuffixByLock(requestParam);
            // 整个短链接  ->  域名 + 短链接
            fullShortUrl = StrBuilder.create(createShortLinkDefaultDomain)
                    .append("/")
                    .append(shortLinkSuffix)
                    .toString();

            ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                    .domain(createShortLinkDefaultDomain)
                    .originUrl(requestParam.getOriginUrl())
                    .gid(requestParam.getGid())
                    .createdType(requestParam.getCreatedType())
                    .validDateType(requestParam.getValidDateType())
                    .validDate(requestParam.getValidDate())
                    .describe(requestParam.getDescribe())
                    .shortUri(shortLinkSuffix)   // 短链接
                    .fullShortUrl(fullShortUrl)
                    .enableStatus(0)   // 是否启用
                    .favicon(getFavicon(requestParam.getOriginUrl()))  // 获取网站的图标 favicon
                    .totalPv(0)
                    .totalUv(0)
                    .totalUip(0)
                    .delTime(0L)
                    .build();
            // 短链接跳转对象
            ShortLinkGoToDO shortLinkGoToDO = ShortLinkGoToDO.builder()
                    .fullShortUrl(fullShortUrl)
                    .gid(requestParam.getGid())
                    .build();
            try {
                baseMapper.insert(shortLinkDO);
                // 路由表的插入
                shortLinkGoToMapper.insert(shortLinkGoToDO);
            } catch (DuplicateKeyException ex) {
                // 数据库判断是否存在该短链接  -  不需要进行判读，布隆过滤器生成短链接时，一定是不存在的
                throw new ServiceException(String.format("短链接：%s 重复生成", fullShortUrl));
            }
            // 新增完整的短链接入布隆过滤器 + 缓存（缓存预热）
            stringRedisTemplate.opsForValue().set(
                    String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, fullShortUrl),
                    requestParam.getOriginUrl(),
                    ShortLinkUtil.getLinkValidTime(requestParam.getValidDate()),
                    TimeUnit.MILLISECONDS
            );
        } finally {
            lock.unlock();
        }
        return ShortLinkCreateRespDTO.builder()
                .gid(requestParam.getGid())
                .originUrl(requestParam.getOriginUrl())
                .fullShortUrl("http://" + fullShortUrl)
                .build();
    }

    /**
     * 通过分布式锁来创建短链接
     */
    private String generateSuffixByLock(ShortLinkCreateReqDTO requestParam) {
        int customGenerateCount = 0;
        // 生成的短链接
        String shortUri;
        while (true) {
            if (customGenerateCount > 10) {
                throw new ServiceException("短链接生成频繁，请稍后再尝试!");
            }
            String originUrl = requestParam.getOriginUrl();
            // 大量请求访问生成短链接时，毫秒可能会导致冲突
            originUrl += UUID.fastUUID().toString();
            shortUri = HashUtil.hashToBase62(originUrl);
            // 直接去数据库里面查询是否有该短链接
            LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                    .eq(ShortLinkDO::getGid, requestParam.getGid())
                    .eq(ShortLinkDO::getFullShortUrl, createShortLinkDefaultDomain + "/" + shortUri)
                    .eq(ShortLinkDO::getDelFlag, 0);
            ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper);
            if (shortLinkDO == null) {
                break;
            }
            customGenerateCount++;
        }
        return shortUri;
    }

    /**
     * 生成短链接  -- 验证短链接是否重复了
     */
    private String generateSuffix(ShortLinkCreateReqDTO requestParam) {
        int customGenerateCount = 0;
        // 生成的短链接
        String shortUri;
        while (true) {
            if (customGenerateCount > 10) {
                throw new ServiceException("短链接生成频繁，请稍后再尝试!");
            }
            String originUrl = requestParam.getOriginUrl();
            // 大量请求访问生成短链接时，毫秒可能会导致冲突
            originUrl += UUID.fastUUID().toString();
            shortUri = HashUtil.hashToBase62(originUrl);
            // 1、短链接确实在缓存中
            // 2、短链接不一定在缓存中，出现布隆过滤器误判问题
            if (!shortUriCreateCachePenetrationBloomFilter.contains(createShortLinkDefaultDomain + "/" + shortUri)) {
                // 不存在
                break;
            }
            // 存在就自增再次生成短链接 - 可能是误判
            customGenerateCount++;
        }
        return shortUri;
    }

    @Override
    public IPage<ShortLinkPageRespDTO> pageShortlink(ShortLinkPageReqDTO requestParam) {
        IPage<ShortLinkDO> resultPage = baseMapper.pageLink(requestParam);
        return resultPage.convert(each -> {
            // 添加域名
            ShortLinkPageRespDTO shortLinkPageRespDTO = BeanUtil.toBean(each, ShortLinkPageRespDTO.class);
            shortLinkPageRespDTO.setDomain("http://" + shortLinkPageRespDTO.getDomain());
            return shortLinkPageRespDTO;
        }); // 转化成分页返回对象
    }

    @Override
    public List<ShortLinkGroupCountQueryRespDTO> listShortLinkGroupQueryCount(List<String> requestParam) {
        QueryWrapper<ShortLinkDO> queryWrapper = Wrappers.query(new ShortLinkDO())
                .select("gid as gid, count(*) as shortLinkGroupCount")
                .in("gid", requestParam)
                .eq("enable_status", 0)
                .eq("del_flag", 0)
                .eq("del_time", 0L)
                .groupBy("gid");
        List<Map<String, Object>> shortLinkDOList = baseMapper.selectMaps(queryWrapper);
        return BeanUtil.copyToList(shortLinkDOList, ShortLinkGroupCountQueryRespDTO.class);
    }


    @Transactional(rollbackFor = Exception.class)    // 出现异常回滚
    @Override
    public void updateShortLink(ShortLinkUpdateReqDTO requestParam) {
        // 验证跳转链接是否添加白名单配置
        verificationWhitelist(requestParam.getOriginUrl());
        // 先查询是否含有这个短链接  -- 如果要改 gid，就找不到这个短链接
        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getGid, requestParam.getOriginGid())
                .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                .eq(ShortLinkDO::getDelFlag, 0)
                .eq(ShortLinkDO::getEnableStatus, 0);// 启用状态
        ShortLinkDO hasShortLinkDO = baseMapper.selectOne(queryWrapper);
        if (hasShortLinkDO == null) {
            // 此时没有这个短链接的信息
            throw new ClientException("数据库中没有该短链接的信息!");
        }
        // 短链接分组标识 gid 没有变更 - 与新的 gid 相比
        if (Objects.equals(hasShortLinkDO.getGid(), requestParam.getGid())) {
            LambdaUpdateWrapper<ShortLinkDO> lambdaUpdateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                    .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                    .eq(ShortLinkDO::getGid, requestParam.getGid())
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    .set(Objects.equals(requestParam.getValidDateType(), ValidDateTypeEnum.PERMANENT.getType()), ShortLinkDO::getValidDate, null);
            ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                    .domain(hasShortLinkDO.getDomain())
                    .shortUri(hasShortLinkDO.getShortUri())
                    .createdType(hasShortLinkDO.getCreatedType())
                    .clickNum(hasShortLinkDO.getClickNum())
                    .favicon(Objects.equals(requestParam.getOriginUrl(), hasShortLinkDO.getOriginUrl()) ? hasShortLinkDO.getFavicon() : getFavicon(requestParam.getOriginUrl()))   // 上面信息都是从原始短链接取
                    .gid(requestParam.getGid())
                    .originUrl(requestParam.getOriginUrl())
                    .describe(requestParam.getDescribe())
                    .validDateType(requestParam.getValidDateType())
                    .validDate(requestParam.getValidDate())   // 上面的信息都是需要修改的信息
                    .build();
            // 短链接基本信息修改
            baseMapper.update(shortLinkDO, lambdaUpdateWrapper);
        } else {
            // 修改 gid 要到写锁
            RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(String.format(RedisKeyConstant.LOCK_GID_UPDATE_KEY, requestParam.getFullShortUrl()));
            RLock writeLock = readWriteLock.writeLock();
            // 为了避免其他线程等待
            writeLock.lock();
            try {
                // 此时 gid 不一致，先删除原始表
                LambdaUpdateWrapper<ShortLinkDO> linkDOLambdaUpdateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                        .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(ShortLinkDO::getGid, hasShortLinkDO.getGid())
                        .eq(ShortLinkDO::getDelFlag, 0)
                        .eq(ShortLinkDO::getDelTime, 0L)
                        .eq(ShortLinkDO::getEnableStatus, 0);
                ShortLinkDO delShortLinkDO = ShortLinkDO.builder()
                        .delTime(System.currentTimeMillis())
                        .build();
                delShortLinkDO.setDelFlag(1);
                baseMapper.update(delShortLinkDO, linkDOLambdaUpdateWrapper);
                // 将新 gid 添加到新表中
                ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                        .domain(createShortLinkDefaultDomain)
                        .originUrl(requestParam.getOriginUrl())
                        .gid(requestParam.getGid())
                        .createdType(hasShortLinkDO.getCreatedType())
                        .validDateType(requestParam.getValidDateType())
                        .validDate(requestParam.getValidDate())
                        .describe(requestParam.getDescribe())
                        .shortUri(hasShortLinkDO.getShortUri())
                        .enableStatus(hasShortLinkDO.getEnableStatus())
                        .totalPv(hasShortLinkDO.getTotalPv())
                        .totalUv(hasShortLinkDO.getTotalUv())
                        .totalUip(hasShortLinkDO.getTotalUip())
                        .fullShortUrl(hasShortLinkDO.getFullShortUrl())
                        .favicon(getFavicon(requestParam.getOriginUrl()))
                        .delTime(0L)
                        .build();
                baseMapper.insert(shortLinkDO);
                // 将原来的路由表也进行删除，添加操作
                LambdaUpdateWrapper<ShortLinkGoToDO> linkGoToLambdaWrapper = Wrappers.lambdaUpdate(ShortLinkGoToDO.class)
                        .eq(ShortLinkGoToDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(ShortLinkGoToDO::getGid, hasShortLinkDO.getGid());
                ShortLinkGoToDO shortLinkGoToDO = shortLinkGoToMapper.selectOne(linkGoToLambdaWrapper);
                shortLinkGoToMapper.delete(linkGoToLambdaWrapper);
                shortLinkGoToDO.setGid(requestParam.getGid());
                shortLinkGoToMapper.insert(shortLinkGoToDO);
            } finally {
                writeLock.unlock();
            }
        }

        // 如果修改了有效期或者有效期时间或者原始链接，删除缓存，重新去数据库去加载
        if (!Objects.equals(hasShortLinkDO.getValidDateType(), requestParam.getValidDateType()) ||
                !Objects.equals(hasShortLinkDO.getValidDate(), requestParam.getValidDate()) ||
                !Objects.equals(hasShortLinkDO.getOriginUrl(), requestParam.getOriginUrl())) {
            stringRedisTemplate.delete(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, requestParam.getFullShortUrl()));
            // 如果设置了有效时间的，并修改为不过期的时间 / 永久有效
            Date currentDate = new Date();
            if (hasShortLinkDO.getValidDate() != null && hasShortLinkDO.getValidDate().before(currentDate)) {
                if (Objects.equals(requestParam.getValidDateType(), ValidDateTypeEnum.PERMANENT.getType()) ||
                        requestParam.getValidDate().after(currentDate)) {
                    // 删除之前的请求缓存
                    stringRedisTemplate.delete(String.format(RedisKeyConstant.GOTO_IS_NULL_SHORT_LINK_KEY, requestParam.getFullShortUrl()));
                }
            }
        }

    }

    @SneakyThrows
    @Override
    public void restoreUrl(String shortUri, ServletRequest request, ServletResponse response) {
        // 获取域名
        String serverName = request.getServerName();
        String serverPort = Optional.of(request.getServerPort())
                // 默认端口号没有加端口号是 80
                .filter(each -> !Objects.equals(each, "80"))
                .map(String::valueOf)
                .map(each -> ":" + each)
                .orElse("");
        String fullShortUrl = serverName + serverPort + "/" + shortUri;
        // 直接从缓存中获取跳转的原始链接
        String orignalUrl = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, fullShortUrl));
        if (StrUtil.isNotBlank(orignalUrl)) {
            shortLinkAccessStats(buildLinkStatsRecordAndSetUser(fullShortUrl, request, response));
            ((HttpServletResponse) response).sendRedirect(orignalUrl);
            return;
        }
        // 通过布隆过滤器去判断是否存在原始跳转短链接
        boolean contains = shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl);
        if (!contains) {
            ((HttpServletResponse) response).sendRedirect("/page/notfound");
            return;
        }
        // 判断请求是否存在
        String gotoIsNullShortLink = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl));
        if (StrUtil.isNotBlank(gotoIsNullShortLink)) {
            ((HttpServletResponse) response).sendRedirect("/page/notfound");
            return;
        }
        // 原始链接为空，去缓存中找
        RLock lock = redissonClient.getLock(String.format(RedisKeyConstant.LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl));
        lock.lock();
        try {
            // 只能有一个请求获取到锁，其他请求获取到之后，直接重定向即可
            orignalUrl = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, fullShortUrl));
            if (StrUtil.isNotBlank(orignalUrl)) {
                shortLinkAccessStats(buildLinkStatsRecordAndSetUser(fullShortUrl, request, response));
                ((HttpServletResponse) response).sendRedirect(orignalUrl);
                return;
            }
            // 防止大量空缓存查询数据库
            gotoIsNullShortLink = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl));
            if (StrUtil.isNotBlank(gotoIsNullShortLink)) {
                ((HttpServletResponse) response).sendRedirect("/page/notfound");
                return;
            }
            // 判断路由表是否存在跳转的长链接
            LambdaQueryWrapper<ShortLinkGoToDO> linkGoToDOLambdaQueryWrapper = Wrappers.lambdaQuery(ShortLinkGoToDO.class)
                    .eq(ShortLinkGoToDO::getFullShortUrl, fullShortUrl);
            ShortLinkGoToDO shortLinkGoToDO = shortLinkGoToMapper.selectOne(linkGoToDOLambdaQueryWrapper);
            if (shortLinkGoToDO == null) {
                // 严谨需要进行封控  - 无这个短链接的原始链接
                stringRedisTemplate.opsForValue().set(
                        String.format(RedisKeyConstant.GOTO_IS_NULL_SHORT_LINK_KEY,
                                fullShortUrl), "-", 30, TimeUnit.MINUTES);
                ((HttpServletResponse) response).sendRedirect("/page/notfound");
                return;
            }
            LambdaQueryWrapper<ShortLinkDO> shortLinkDOLambdaQueryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                    .eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
                    .eq(ShortLinkDO::getGid, shortLinkGoToDO.getGid())
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .eq(ShortLinkDO::getEnableStatus, 0);
            ShortLinkDO shortLinkDO = baseMapper.selectOne(shortLinkDOLambdaQueryWrapper);
            // 判断时间是否过期
            if (shortLinkDO == null || shortLinkDO.getValidDate() != null && shortLinkDO.getValidDate().before(new Date())) {
                // 严谨需要进行封控  - 无这个短链接的原始链接
                stringRedisTemplate.opsForValue().set(
                        String.format(RedisKeyConstant.GOTO_IS_NULL_SHORT_LINK_KEY,
                                fullShortUrl), "-", 30, TimeUnit.MINUTES);
                ((HttpServletResponse) response).sendRedirect("/page/notfound");
                return;
            }
            // 直接进行缓存预热
            stringRedisTemplate.opsForValue().set(
                    String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, fullShortUrl),
                    shortLinkDO.getOriginUrl(),
                    ShortLinkUtil.getLinkValidTime(shortLinkDO.getValidDate()),
                    TimeUnit.MILLISECONDS
            );
            // 将原始链接进行重定向
            shortLinkAccessStats(buildLinkStatsRecordAndSetUser(fullShortUrl, request, response));
            ((HttpServletResponse) response).sendRedirect(shortLinkDO.getOriginUrl());
        } finally {
            lock.unlock();
        }
    }

    private ShortLinkStatsRecordDTO buildLinkStatsRecordAndSetUser(String fullShortUrl, ServletRequest request, ServletResponse response) {
        // cookie 是否重复存在标志位 - 存在说明是多次访问
        AtomicBoolean uvFirstFlag = new AtomicBoolean();
        // 判断 cookie 是否存在
        Cookie[] cookies = ((HttpServletRequest) request).getCookies();
        AtomicReference<String> uv = new AtomicReference<>();
        Runnable addResponseCookieTask = () -> {
            uv.set(UUID.fastUUID().toString());
            Cookie uvCookie = new Cookie("uv", uv.get());
            uvCookie.setMaxAge(60 * 60 * 24 * 30); // 设置cookie 过期时间为 30 天
            uvCookie.setPath(StrUtil.sub(fullShortUrl, fullShortUrl.indexOf("/"), fullShortUrl.length())); //设置 cookie 路径
            ((HttpServletResponse) response).addCookie(uvCookie);
            uvFirstFlag.set(Boolean.TRUE);
            stringRedisTemplate.opsForSet().add(SHORT_LINK_STATS_UV_KEY + fullShortUrl, uv.get());
        };
        if (ArrayUtil.isNotEmpty(cookies)) {
            Arrays.stream(cookies)
                    .filter(each -> Objects.equals(each.getName(), "uv"))
                    .findFirst()
                    .map(Cookie::getValue)
                    .ifPresentOrElse(each -> {
                        uv.set(each);
                        // 直接添加到 redis 的set集合中，进行去重效果
                        Long added = stringRedisTemplate.opsForSet().add(SHORT_LINK_STATS_UV_KEY + fullShortUrl, each);
                        uvFirstFlag.set(added != null && added > 0L);
                    }, addResponseCookieTask);
        } else {
            addResponseCookieTask.run();
        }
        // 维护 uip 是否重复
        String remoteAddress = ShortLinkUtil.getActualIP((HttpServletRequest) request);
        // 短链接操作系统访问监控
        String os = ShortLinkUtil.getUserOS((HttpServletRequest) request);
        // 浏览器
        String browser = ShortLinkUtil.getBrowser((HttpServletRequest) request);
        // 设备
        String device = ShortLinkUtil.getDevice((HttpServletRequest) request);
        // 网络
        String netWork = ShortLinkUtil.getNetwork((HttpServletRequest) request);
        // 将 uip 添加到 redis 的set集合中，进行去重操作
        Long uipAdded = stringRedisTemplate.opsForSet().add(SHORT_LINK_STATS_UIP_KEY + fullShortUrl, remoteAddress);
        boolean uipFirstFlag = uipAdded != null && uipAdded > 0L;
        return ShortLinkStatsRecordDTO.builder()
                .fullShortUrl(fullShortUrl)
                .uv(uv.get())
                .uvFirstFlag(uvFirstFlag.get())
                .uipFirstFlag(uipFirstFlag)
                .remoteAddr(remoteAddress)
                .os(os)
                .browser(browser)
                .device(device)
                .network(netWork)
                .currentDate(new Date())
                .build();
    }

    @SneakyThrows
    public void shortLinkAccessStats(ShortLinkStatsRecordDTO statsRecord) {
        HashMap<String, String> producerMap = new HashMap<>();
        producerMap.put("statsRecord", JSON.toJSONString(statsRecord));
        shortLinkStatsSaveProducer.send(producerMap);
    }

    /**
     * 获取网站图标 favicon
     */
    @SneakyThrows
    private String getFavicon(String url) {
        URL targetUrl = new URL(url);
        HttpURLConnection urlConnection = (HttpURLConnection) targetUrl.openConnection();
        urlConnection.setRequestMethod("GET");
        urlConnection.connect();
        int responseCode = urlConnection.getResponseCode();
        if (responseCode == HttpURLConnection.HTTP_OK) {
            Document document = Jsoup.connect(url).get();
            // 查找 <link> 标签中包含 rel="icon" 或 rel="shortcut icon" 的元素
            Element iconLink = document.select("link[rel~=(?i)^(icon|shortcut icon)$]").first();
            if (iconLink != null) {
                return iconLink.attr("abs:href");
            }
        }
        return null;
    }

    /**
     * 验证原始跳转链接是否配置白名单
     */
    public void verificationWhitelist(String originUrl) {
        Boolean enable = gotoDomainWhiteListConfiguration.getEnable();
        if (enable == null || !enable) {
            return;
        }
        // 获取域名
        String domain = ShortLinkUtil.extractDomain(originUrl);
        if (StrUtil.isBlank(domain)) {
            throw new ClientException("跳转链接填写错误");
        }
        List<String> whiteListDetails = gotoDomainWhiteListConfiguration.getDetails();
        if (!whiteListDetails.contains(domain)) {
            throw new ClientException("演示环境为避免恶意攻击，请生成以下网站跳转链接：" + gotoDomainWhiteListConfiguration.getNames());
        }
    }
}
